/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.core.component.io;

import com.vii.brillien.core.component.time.CyclicPresence;
import com.vii.brillien.core.management.BrillienServices;
import com.vii.brillien.core.management.component.MetaServices;
import com.vii.brillien.core.management.component.PresenceServices;
import com.vii.brillien.core.management.io.IOServices;
import com.vii.brillien.core.management.io.jar.JarUnpacker;
import com.vii.brillien.core.management.io.jar.JarValidator;
import com.vii.brillien.ignition.BrillienStatics;
import com.vii.brillien.ignition.classloading.PresenceClassLoader;
import com.vii.brillien.kernel.BrillienException;
import com.vii.brillien.kernel.annotations.PresenceService;
import com.vii.brillien.kernel.annotations.lifecycle.Resident;
import com.vii.streamline.structures.collections.InnerList;
import com.vii.streamline.structures.collections.InnerMap;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * A Presence to observe FS changes
 */
@PresenceService( logLevel = PresenceService.FINEST, periodical = true )
@Resident
public class FileWatcher<R> extends CyclicPresence<R> {

    /**
     * Synchronization tool to separate the 2 phases of presence loading process
     */
    protected CountDownLatch jobCountDownLatch;

    /**
     * File reference to a folder to watch
     */
    protected File              fileToWatch;

    /**
     * FileFilter to identify the file types to be checked
     */
    protected FileFilter        filter;

    /**
     * Collection of chached information about loaded files
     */
    protected List<VFile>       knownFiles;


    @Override
    public void reset() {
        super.reset();

        waitingForInputData = true;
    }

    /**
     * Cache information about loaded files
     */
    protected static class VFile{
        private String      name;
        private long        modificationDate;
        private long        size;

        /**
         * Creates new VFile instance
         */
        public VFile( File f ) {
            name = f.getName();
            modificationDate = f.lastModified();
            size = f.length();
        }

        /**
         * Tells whether a cached information is in line with a real file
         */
        public boolean equals(File f) {
            return  f.getName().equals( name ) &&
                    f.lastModified() == modificationDate &&
                    size == f.length();
        }

        /**
         * Tells whether a cached information is in line with a real file
         */
        public boolean equals(VFile f) {
            return  f.name.equals( name ) &&
                    f.modificationDate == modificationDate &&
                    size == f.size;
        }

        @Override
        public boolean equals(Object obj) {
            return  obj instanceof VFile
                    ? equals( (VFile)obj )
                    : (obj instanceof File
                    ? equals( (File)obj )
                    : false);
        }

        public String getName() {
            return name;
        }

        public long getModificationDate() {
            return modificationDate;
        }

        public long getSize() {
            return size;
        }

        /**
         * Tells whether the actual VFile instance has the same name as the given file
         */
        public boolean isModified(File f) {
            return f.getName().equals( name );
        }

        public boolean isModified(VFile f) {
            return f.getName().equals( name );
        }

        @Override
        public String toString() {
            return name + " " + modificationDate + " " +  size;
        }
    }

    public void setFileToWatch(File fileToWatch) throws BrillienException {
        this.fileToWatch = fileToWatch;

        if( !fileToWatch.isDirectory() ) throw new BrillienException("Only Directory changes is allowed to monitor...");

        filter = new FileFilter() {
            public boolean accept(File file) {
                return !file.isDirectory() && file.getName().endsWith(".jar") && file.length()>0 && file.canRead() && file.canWrite();
            }
        };
        knownFiles = new LinkedList<VFile>();
    }

    protected boolean contains( File file ){
        for( VFile vfile : knownFiles ){
            if( vfile.equals( file ) )
                return true;
        }
        return false;
    }

    protected void removePresence( VFile file ){
        File extractDir = new File(
            BrillienStatics.ACTIVATOR_DIR + File.separator + file.getName().substring( 0, file.getName().lastIndexOf(".") )
        );

        String archiveReferenceName = extractDir.getName();

        // shutdown classloader, presences, etc.
        PresenceClassLoader loader = MetaServices.getClassLoader( archiveReferenceName );

        List<String> Presences = MetaServices.getActivatedPresences( archiveReferenceName );
        log( "Getting activated presences: ", new InnerList<Object>( Presences ) );
        
        for( String key : Presences ){
            log( "Removing presence: " + key );
            try {
                Class PresenceClass = loader.loadClass( key );
                String PresenceName = PresenceServices.getPresenceName( PresenceClass );
                BrillienServices.liaison.unregisterPresence( PresenceName );
            } catch (Exception e) {
                exceptionLog( FileWatcher.class.getSimpleName(), "removePresence", e );                
            }
        }

        loader.done();

        // erase extracted directory
        IOServices.delete( extractDir );

        knownFiles.remove( file );
    }
    protected void checkReplacement( File[] files ){
        List<VFile> references = new LinkedList<VFile>();

        for( File file : files ) {
            VFile temp = new VFile( file );
            VFile ref = null;
            for( VFile f : knownFiles ){
                if( f.isModified( temp ) ){
                    ref = f;
                    break;
                }
            }
            if( ref != null && ref.getSize()>0 && ref.getModificationDate() != temp.getModificationDate() )
                references.add( ref );
        }
        for( VFile removedFile : references ){
            log( "Module removing for republishing process: " + removedFile );
            removePresence( removedFile );
        }
    }
    protected void checkDeletion( File[] files ){
        List<VFile> references = new LinkedList<VFile>();
        for( VFile f : knownFiles )
            references.add( f );

        for( File file : files ) {
            VFile temp = new VFile( file );
            if( references.contains( temp ) )
                references.remove( references.indexOf( temp ) );
        }

        for( VFile removedFile : references ){
            log( "Module removing caused by archieve deletion: " + removedFile );
            removePresence( removedFile );
        }
    }

    @Override
    protected R innerCall() throws BrillienException{
        File[] files = fileToWatch.listFiles( filter );

        log( "Checking deletion..." );
        checkDeletion( files );

        log( "Checking replacement..." );
        checkReplacement( files );

        log( "Searching for new files..." );
        LinkedList<File> list = new LinkedList<File>();
        for( File file : files ) {
            if( !contains(file) ){
                list.add( file );
            }
        }

        jobCountDownLatch = new CountDownLatch( list.size() );

        for( File file : list ) {
            log( "New unregistered archieve found:: ", new InnerList<Object>( file ) );

            VFile vFile = null;
            for( VFile vfile : knownFiles ){
                if( vfile.isModified(file) ){
                    log( "Modified archieve found:: ", new InnerList<Object>( file.getName() ) );
                    vFile = vfile;
                    break;
                }
            }
            try {
                log( "Archieve validation checking..." );

                if( !JarValidator.isValid(file) )
                    throw new IOException( "Invalid Jar file:: " + file );

                log( "Archieve is valid." );

                File extractDir = new File(
                    BrillienStatics.ACTIVATOR_DIR + File.separator + file.getName().substring( 0, file.getName().lastIndexOf(".") )
                );

                if( vFile != null ){
                    removePresence( vFile );
                }

                JarUnpacker.unArchive( file, extractDir );

                String archiveReferenceName = extractDir.getName();
                String jobID = BrillienServices.nextJobID();
                sendAGet( "presenceloader", "load", jobID, new InnerMap<String, Object>( "containerDirPath", extractDir.getAbsolutePath(), "jarReferenceName", archiveReferenceName ) );
                log( "Archieve loading job is sent:" + jobID );
            } catch (IOException e) {
                exceptionLog( "FileWatcher", "innerRun", e );
            }
        }

        try {
            jobCountDownLatch.await();
        } catch (InterruptedException e) {
            exceptionLog( FileWatcher.class.getName(), "innerCall", e );
        }

        for( File file : list ) {
            log( "Activating archieve:: ", new InnerList<Object>( file.getName() ) );

            File extractDir = new File(
                BrillienStatics.ACTIVATOR_DIR + File.separator + file.getName().substring( 0, file.getName().lastIndexOf(".") )
            );

            String archiveReferenceName = extractDir.getName();
            String jobID = BrillienServices.nextJobID();
            sendAGet( "presenceloader", "activatePresences", jobID, new InnerMap<String, Object>( "jarReferenceName", archiveReferenceName ) );

            log( "Archieve activation job is sent:" + jobID );
        }

        for( File file : list ) {
            knownFiles.add( new VFile( file ) );
        }

        return null;
    }

    @Override
    public Object processMessage(String message, Object... parameters) {
        log( "Archieve job is done:" + message );

        if( jobCountDownLatch != null )
            jobCountDownLatch.countDown();

        return null;
    }
    
}

